#!/usr/bin/env bash

######################################################################
# @author      : alpha (alpha@mascot)
# @created     : Saturday May 01, 2021 21:58:49 CST
#
# @description : cross compile linux kernel/module, busybox, u-boot, etc.
######################################################################

#
# see: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
#                    | parameter Set and Not Null | parameter Set But Null | parameter Unset
# ${parameter:-word} | substitute parameter       | substitute word        | substitute word
# ${parameter-word}  | substitute parameter       | substitute null        | substitute word
# ${parameter:=word} | substitute parameter       | assign word            | assign word
# ${parameter=word}  | substitute parameter       | substitute null        | assign word
# ${parameter:?word} | substitute parameter       | error, exit            | error, exit
# ${parameter?word}  | substitute parameter       | substitute null        | error, exit
# ${parameter:+word} | substitute word            | substitute null        | substitute null
# ${parameter+word}  | substitute word            | substitute word        | substitute null
#

# default values
DEFAULT_ARCH=riscv
DEFAULT_CROSS_COMPILE=riscv64-linux-gnu-
DEFAULT_OPT="-j $(nproc)"
OUTPUT_BASE=./${DEFAULT_CROSS_COMPILE/-*}

# default option arguments
ARG_C=
ARG_M=
ARG_O=
ARG_git=
ARG_GIT=
ARG_SUFFIX=

function usage()
{
	echo -e "A tool to cross compile linux kernel, module, tool, busybox, u-boot, etc."
	echo -e "Usually used for out-of-tree building."
	echo -e
	echo -e "usage: $0 [OPTIONS]"
	echo -e
	echo -e "OPTIONS:"
	echo -e "\t-h, print this usage and exit"
	echo -e "\t-C <change to directory>, pass to make -C option"
	echo -e "\t   such as:"
	echo -e "\t   <source directory of kernel, busybox, u-boot, tool, etc.>"
	echo -e "\t-M <module source directory>, pass to Makefile M variable to build module"
	echo -e "\t-O [optional build output directory], pass to Makefile O variable"
	echo -e "\t\tdefault: <realpath of ./<basename of <change to directory>>>"
	echo -e "\t-git, if -C argument is a git repository, append git HEAD to the build output directory"
	echo -e "\t-GIT, same as -git but append dirty suffix"
	echo -e "\t-suffix <build output directory suffix>, append suffix to the build output directory"
	echo -e "\t-- [optional extra arguments pass to make or Makefile, i.e. make targets]"
	echo -e
	echo -e "EXAMPLES:"
	echo -e "\tbuild kernel:"
	echo -e "\t$0 -C ../linux/linux-5.10.23 -O -- mrproper"
	echo -e "\t$0 -C ../linux/linux-5.10.23 -- defconfig all"
	echo -e "\tbuild module:"
	echo -e "\t$0 -C ../linux/linux-5.10.23 -M ../modules/hello-world"
	echo -e "\tbuild busybox:"
	echo -e "\t$0 -C ../busybox/busybox-1.32.1 -- menuconfig"
	echo -e "\t$0 -C ../busybox/busybox-1.32.1 -- install"
	echo -e "\tbuild perf:"
	echo -e "\t$0 -C ../linux/linux-5.10.23/tools/perf/ -- LDFLAGS=-static"
	echo -e
}

# parse arguments
while [[ -n "$1" ]]; do
	# if $1 is last argument, set arg=--, otherwise set arg=$2
	arg=${2:---}
	case "$1" in
	"-C")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_C=$arg
			shift 2
		fi
		;;
	"-M")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_M=$arg
			shift 2
		fi
		;;
	"-O")
		if [[ $arg == -* ]]; then
			unset ARG_O
			shift 1
		else
			ARG_O=$arg
			shift 2
		fi
		;;
	"-suffix")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_SUFFIX=$arg
			shift 2
		fi
		;;
	"-git")
		ARG_git=y
		shift 1
		;;
	"-GIT")
		ARG_GIT=y
		shift 1
		;;
	"--")
		shift
		ARG_EXTRA="$@"
		break
		;;
	"-h")
		usage
		exit 0
		;;
	*)
		echo -e "unknown option: $1\n"
		usage
		exit -1
		;;
	esac
done

# if ARG_C is set and not null, set REAL_C
if [[ -n "${ARG_C:+x}" ]]; then
	# remove all suffix '/'
	# ARG_C=${ARG_C/%+(\/)} NOT work in script but in command line, why?
	ARG_C=$(echo "${ARG_C}" | sed 's/\/\+$//')

	# if ARG_C is null, set default value as '.'
	ARG_C=${ARG_C:-.}

	REAL_C="$(realpath -e ${ARG_C})"
	if [[ -z "${REAL_C}" ]]; then
		echo
		usage
		exit -1
	fi
fi

# if ARG_M is set and not null, set REAL_M
if [[ -n "${ARG_M:+x}" ]]; then
	# remove all suffix '/'
	ARG_M=$(echo "${ARG_M}" | sed 's/\/\+$//')

	# if ARG_M is null, set default value as '.'
	ARG_M=${ARG_M:-.}

	REAL_M="$(realpath -e ${ARG_M})"
	if [[ -z "${REAL_M}" ]]; then
		echo
		usage
		exit -1
	fi
fi

#
# about REAL_O
#
# if ARG_O unset:
#         unset REAL_O
# if ARG_O set and not null:
#         if ARG_O begin with "./", REAL_O set realpath ARG_O, or ARG_O
# if ARG_O set but null:
#         if REAL_C not null, REAL_O set by REAL_C, or null
#
if [[ -z "${ARG_O+x}" ]]; then
	# ARG_O unset
	unset REAL_O
elif [[ -n "${ARG_O:+x}" ]]; then
	# ARG_O set and not null
	if [[ ${ARG_O} == ./* ]]; then
		# ARG_O begin with "./"
		REAL_O="$(realpath -m ${OUTPUT_BASE:-.}/${ARG_O})"
	else
		REAL_O="${ARG_O}"
		# remove all suffix '/'
		REAL_O=$(echo "${REAL_O}" | sed 's/\/\+$//')
	fi
else
	# ARG_O set but null
	REAL_O=${REAL_C:+"$(realpath -m ${OUTPUT_BASE:-.}/$(basename ${REAL_C}))"}
fi

function git_head()
{
	branch=$(git -C "$1" rev-parse --abbrev-ref HEAD 2>/dev/null)
	commit=$(git -C "$1" rev-parse --short HEAD 2>/dev/null)
	status=$([[ "$2" == "y" ]] && [[ -n $(git -C "$1" status -s 2>/dev/null) ]] && echo "dirty")
	if [[ -n "${branch:+x}" ]]; then
		echo "${branch}${commit:+-${commit}}${status:+-${status}}"
	elif [[ -n "${commit:+x}" ]]; then
		echo "${commit}${status:+-${status}}"
	else
		echo ""
	fi
}

# if REAL_O is set and not null, append suffix
if [[ -n "${REAL_O:+x}" ]]; then
	git_suffix=$([[ "${ARG_git:-}" == "y" || "${ARG_GIT:-}" == "y" ]] && \
		     [[ -n "${REAL_C:+x}" ]] && \
		     git_head "${REAL_C}" "${ARG_GIT}")
	REAL_O="${REAL_O}${git_suffix:+-${git_suffix}}"
	REAL_O="${REAL_O}${ARG_SUFFIX:+-${ARG_SUFFIX}}"
fi

# set make -C options
OPT_C=${REAL_C:+"-C ${REAL_C}"}

# set Makefile variable M
OPT_M=${REAL_M:+"M=${REAL_M}"}

# set Makefile variable O
OPT_O=${REAL_O:+"O=${REAL_O}"}

OPT_ARCH=${DEFAULT_ARCH:+"ARCH=${DEFAULT_ARCH}"}
OPT_CROSS_COMPILE=${DEFAULT_CROSS_COMPILE:+"CROSS_COMPILE=${DEFAULT_CROSS_COMPILE}"}

########

MAKE_OPT="${MAKE_OPT} ${DEFAULT_OPT}"
MAKE_OPT="${MAKE_OPT} ${OPT_ARCH}"
MAKE_OPT="${MAKE_OPT} ${OPT_CROSS_COMPILE}"
MAKE_OPT="${MAKE_OPT} ${OPT_C}"
MAKE_OPT="${MAKE_OPT} ${OPT_O}"
MAKE_OPT="${MAKE_OPT} ${OPT_M}"
MAKE_OPT="${MAKE_OPT} ${ARG_EXTRA}"

MAKE_CMD="make ${MAKE_OPT}"
echo ${MAKE_CMD}

if [[ ${REAL_O} == /* ]]  && [[ ! -d "${REAL_O}" ]]; then
	echo -e "directories '${REAL_O}' will be created simultaneously"
fi

# waiting for confirmation
echo
TMOUT=3
read -p "Continue in ${TMOUT} seconds? [Y/n]: " -t ${TMOUT} input
if [[ "${input:-}" != "" && "${input:-}" != "y" && "${input:-}" != "Y" ]]; then
	echo -e "\nCancel"
	exit 0
fi
echo

########

# create output directories
if [[ ${REAL_O} == /* ]]  && [[ ! -d "${REAL_O}" ]]; then
	MKDIR_O="mkdir -p ${REAL_O}"
	${MKDIR_O}
	if [ $? -eq 0 ]; then
		echo "'${MKDIR_O}': Succeeded"
	else
		echo "'${MKDIR_O}': Failed"
		exit -1
	fi
fi

# run make now
${MAKE_CMD}
